package tv.geir.slackspace.server;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.SocketException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import tv.geir.slackspace.common.CallbackPoint;
import tv.geir.slackspace.common.ConnectionException;
import tv.geir.slackspace.communication.InvalidResponseException;
import tv.geir.slackspace.communication.Request;
import tv.geir.slackspace.communication.Response;
import tv.geir.slackspace.event.SpaceEvent;
import tv.geir.slackspace.monitor.PerformanceMonitor;
import tv.geir.slackspace.monitor.PerformanceMonitor.MonitorKey;
import tv.geir.slackspace.space.IRequestProcessor;

public class SocketHandler implements ISocketHandler, ClientEventDispatcher {

	private Socket connection;

	private static Log log = LogFactory.getLog(SocketHandler.class);

	private IRequestProcessor requestProcessor;

	private CallbackPoint callbackPoint;
	
	private int callbackFailedCount = 0;
	
	private int callbackCount = 0;
	
	private PerformanceMonitor performanceMonitor;

	/**
	 
	 */
	public SocketHandler() {
		
	}

	/**
	 * Starts processing of connection.
	 */
	public void run() {
		handleConnection(connection);
	}

	/**
	 * 
	 * Handles all requests for given connection until client disconnects.
	 * 
	 * @param connection
	 */
	private void handleConnection(Socket connection) {
		if( log.isDebugEnabled() ) {
		log.debug("Handling connection to: " + connection.getInetAddress());
		}
		MonitorKey key = null;
		try {
			ObjectInputStream ois = new ObjectInputStream( 
					connection.getInputStream());
			ObjectOutputStream oos = new ObjectOutputStream(connection
					.getOutputStream());
			//Object o;

			/*do {
				callbackPoint = readCallbackInfo(ois, oos);
			} while (callbackPoint == null);
			*/
			while (connection.isConnected()) {
				
				
				readRequestFromSocketThenProcessAndReply( ois, oos );
				
			}

		} catch( SocketException se ) {
			log.warn( "Client disconnected abnormally: " + se.getMessage() );
		} catch (IOException e) {
			if (connection.isClosed()) {
				// closed normally?
				if( log.isDebugEnabled() ) {
					log.debug("Connection closed ", e);
				}
			} else {
				// some error occured?
				log.warn( "Error handling connection: " + e.getMessage(), e );
				throw new ConnectionException(e);
			}
		} catch (ClassNotFoundException cnfe) {
			log.error( "Processing socket: " + cnfe.getMessage(), cnfe );
			throw new ConnectionException(cnfe);
		} finally {
			// make sure connection is closed when exiting thread
			try {
				if (connection.isConnected()) {
					connection.close();
				}
			} catch (IOException ioe) {
			}
			log.debug("Finished handling connection to: "
					+ connection.getInetAddress());
			if( key != null ) {
				performanceMonitor.failMonitoring( key );
			}
		}
	}

	/**
	 * Reads a request from the input stream, processes the request and writes
	 * response back to the output stream.
	 * @param ois stream to read from
	 * @param oos stream to write to
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	private void readRequestFromSocketThenProcessAndReply(ObjectInputStream ois, 
			ObjectOutputStream oos) throws ClassNotFoundException, IOException{
		Request req = readRequest(ois, oos);
		
		Response res = requestProcessor.process(req);
		 
		oos.writeObject(res);
	}
	
	/**
	 * Reads CallbackInformation from the inputstream. If the object read is not
	 * a CallbackInformation instance a error response is written to the output
	 * stream.
	 * 
	 * @param ois
	 * @param oos
	 * @return
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	private CallbackPoint readCallbackInfo(ObjectInputStream ois,
			ObjectOutputStream oos) throws IOException, ClassNotFoundException {
		Object o = ois.readObject();
		CallbackPoint cb = null;
		if (o instanceof CallbackPoint) {
			cb = (CallbackPoint) o;
		} else {
			InvalidResponseException pve = new InvalidResponseException(
					"Expected CallbackInformation");
			Response res = new Response(Response.Code.ERROR, null);
			res.setError(pve);
			log.warn(pve);
			oos.writeObject(res);

		}
		return cb;
	}

	public Request readRequest(ObjectInputStream ois, ObjectOutputStream oos)
			throws IOException, ClassNotFoundException {
		// read request
		Object o = ois.readObject();
		Request req = null;

		if (o instanceof Request) {
			// execute request and keep response
			req = (Request) o;
		} else {
			// not a request object, create response indicating error
			Response res = new Response(Response.Code.ERROR, null);
			InvalidResponseException e = new InvalidResponseException(
					"Invalid request, expected a Request object");
			res.setData(e);
			// log problem
			log.warn(e);
			// send error to client
			oos.writeObject(res);
		}
		if (req == null) {
			log.warn("Request empty, reentering read request");
			return readRequest(ois, oos);
		}
		return req;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see tv.geir.slackspace.server.ISocketHandler#closeConnection()
	 */
	public void closeConnection() {
		if (connection.isClosed()) {
			return;
		}

		Response res = new Response(Response.Code.SHUTDOWN, null);

		try {
			ObjectOutputStream oos = new ObjectOutputStream(connection
					.getOutputStream());
			oos.writeObject(res);
			oos.close();
			connection.close();
		} catch (IOException e) {
			log.warn("Error disconnecting client: "
					+ connection.getInetAddress()
					+ ". Connection already closed?", e);
		}

	}

	/**
	 * Publishes the given event to the callback listener.
	 */
	public void publishEvent(SpaceEvent evt) {
		callbackCount++;
		try {
			// find connection to listener
			Socket con;
			if (connection.isConnected()) {
				// we can use the same connection
				con = connection;
			} else {
				// not connected any longer. use callback information
				// to establish a new connection to the listener
				con = new Socket(callbackPoint.getAddress(), callbackPoint
						.getPort());
				connection = con;
			}
			
			// wrap event as a response
			Response res = new Response( Response.Code.EVENT, null );
			res.setData( evt );
			ObjectOutputStream oos = new ObjectOutputStream( con.getOutputStream() );
			
			// send it
			oos.writeObject( res );
		} catch (Throwable t) {
			log.warn( "Failed publishing event, client may have shut down: " + t.getMessage() );
			callbackFailedCount++;
		} 
	}

	public void setSocket(Socket socket) {
		this.connection = socket;
		
	}

	public void setRequestProcessor(IRequestProcessor requestProcessor) {
		this.requestProcessor = requestProcessor;
	}
}
